<template>
  <div class="family-tree-container">
    <h2>关系图</h2>
    <div ref="containerRef" class="family-tree-graph"/>
  </div>
</template>

<script setup>
import { Badge, BaseBehavior, ExtensionCategory, Graph, GraphEvent, Label, Rect, register } from '@antv/g6'

import { familyData } from './data.js'

const statusColors = {
  online: '#17BEBB',
  busy: '#E36397',
  offline: '#B7AD99'
}

const DEFAULT_LEVEL = 'detailed'

const containerRef = ref(null)

/**
 * Draw a chart node with different ui based on the zoom level.
 */
class ChartNode extends Rect {
  get data() {
    return this.context.model.getElementDataById(this.id).data
  }

  get level() {
    return this.data.level || DEFAULT_LEVEL
  }

  getLabelStyle() {
    const text = this.data.name + this.data.gender
    const labelStyle =
      this.level === 'overview'
        ? {
          fill: '#fff',
          fontSize: 20,
          fontWeight: 600,
          textAlign: 'center',
          transform: [['translate', 0, 0]]
        }
        : {
          fill: '#2078B4',
          fontSize: 14,
          fontWeight: 400,
          textAlign: 'left',
          transform: [['translate', -65, -15]]
        }
    return { text, ...labelStyle }
  }

  getKeyStyle(attributes) {
    return {
      ...super.getKeyStyle(attributes),
      fill: this.level === 'overview' ? statusColors[this.data.status] : '#fff'
    }
  }

  getPositionStyle(attributes) {
    if (this.level === 'overview') return false
    return {
      text: this.data.position,
      fontSize: 8,
      fontWeight: 400,
      textTransform: 'uppercase',
      fill: '#343f4a',
      textAlign: 'left',
      transform: [['translate', -65, 0]]
    }
  }

  drawPositionShape(attributes, container) {
    const positionStyle = this.getPositionStyle(attributes)
    this.upsert('position', Label, positionStyle, container)
  }

  getStatusStyle(attributes) {
    if (this.level === 'overview') return false
    return {
      text: this.data.status,
      fontSize: 8,
      textAlign: 'left',
      transform: [['translate', 40, -16]],
      padding: [0, 4],
      fill: '#fff',
      backgroundFill: statusColors[this.data.status]
    }
  }

  drawStatusShape(attributes, container) {
    const statusStyle = this.getStatusStyle(attributes)
    this.upsert('status', Badge, statusStyle, container)
  }

  getPhoneStyle(attributes) {
    if (this.level === 'overview') return false
    return {
      text: this.data.phone,
      fontSize: 8,
      fontWeight: 300,
      textAlign: 'left',
      transform: [['translate', -65, 20]]
    }
  }

  drawPhoneShape(attributes, container) {
    const style = this.getPhoneStyle(attributes)
    this.upsert('phone', Label, style, container)
  }

  render(attributes = this.parsedAttributes, container = this) {
    super.render(attributes, container)

    this.drawPositionShape(attributes, container)
    this.drawStatusShape(attributes, container)
    this.drawPhoneShape(attributes, container)
  }
}

/**
 * Implement a level of detail rendering, which will show different details based on the zoom level.
 */
class LevelOfDetail extends BaseBehavior {
  prevLevel = DEFAULT_LEVEL
  levels = {
    'overview': [0, 0.6],
    'detailed': [0.6, Infinity]
  }

  constructor(context, options) {
    super(context, options)
    this.bindEvents()
  }

  update(options) {
    this.unbindEvents()
    super.update(options)
    this.bindEvents()
  }

  updateZoomLevel = async(e) => {
    if ('scale' in e.data) {
      const scale = e.data.scale
      const level = Object.entries(this.levels).find(([key, [min, max]]) => scale > min && scale <= max)?.[0]
      if (level && this.prevLevel !== level) {
        const { graph } = this.context
        graph.updateNodeData((prev) => prev.map((node) => ({ ...node, data: { ...node.data, level } })))
        await graph.draw()
        this.prevLevel = level
      }
    }
  }

  bindEvents() {
    const { graph } = this.context
    graph.on(GraphEvent.AFTER_TRANSFORM, this.updateZoomLevel)
  }

  unbindEvents() {
    const { graph } = this.context
    graph.off(GraphEvent.AFTER_TRANSFORM, this.updateZoomLevel)
  }

  destroy() {
    this.unbindEvents()
    super.destroy()
  }
}

register(ExtensionCategory.NODE, 'chart-node', ChartNode)
register(ExtensionCategory.BEHAVIOR, 'level-of-detail', LevelOfDetail)

onMounted(() => {
  const graph = new Graph({
    container: containerRef.value,
    data: familyData,
    node: {
      type: 'chart-node',
      style: {
        labelPlacement: 'center',
        lineWidth: 1,
        ports: [{ placement: 'top' }, { placement: 'bottom' }],
        radius: 2,
        shadowBlur: 10,
        shadowColor: '#e0e0e0',
        shadowOffsetX: 3,
        size: [150, 60],
        stroke: '#C0C0C0'
      }
    },
    edge: {
      type: 'polyline',
      style: {
        router: {
          type: 'orth'
        },
        stroke: '#C0C0C0'
      }
    },
    layout: {
      type: 'dagre'
    },
    autoFit: 'view',
    behaviors: ['level-of-detail', 'zoom-canvas', 'drag-canvas']
  })

  graph.render()
})
</script>

<style scoped>
.family-tree-container {
  padding: 20px;
  width: 100%;
  height: 100%;
  box-sizing: border-box;
  background-color: #000;
}

.family-tree-graph {
  overflow: hidden;
  width: 100%;
  height: calc(100vh - 100px);
  border: 1px solid #e2e8f0;
  border-radius: 8px;
}

h2 {
  margin-bottom: 20px;
  font-size: 24px;
  color: #333;
}
</style>
